package com.goodwin.deity.desktop.controller;

import com.goodwin.deity.desktop.domain.ImageEntry;
import com.goodwin.deity.desktop.util.CompareImages;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.shape.Box;
import javafx.stage.DirectoryChooser;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class DesktopController implements Initializable {

    private static final String IMAGE_PATTERN = "(.+(\\.(?i)(jpg|png|bmp))$)";
    private static List<File> foundImages = new ArrayList<>();
    private final Logger log = Logger.getLogger(this.getClass().getName());
    @FXML
    private Button btnBrowse;
    @FXML
    private TextField lblDirectory;
    @FXML
    private ProgressBar progressBar;
    private int progressBarMax = 0;
    private int progressBarCurrent = 0;
    @FXML
    private TableView<ImageEntry> tblResults;
    @FXML
    private TableColumn<ImageEntry, String> clmImageName;
    @FXML
    private TableColumn<ImageEntry, String> clmDuplicateCount;
    @FXML
    private ImageView imgResult;
    @FXML
    private ScrollPane sPaneResults;
    @FXML
    private CheckBox cbxSubFolders;
    @FXML
    private Slider slidThreshold;
    @FXML
    private Label lblProgress;
    @FXML
    private Button btnSearch;
    private ObservableList<ImageEntry> imageSearchResults = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
    private List<ImageEntry> duplicateImages = new ArrayList<>();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        clmImageName.setCellValueFactory(cellData -> cellData.getValue().getSimpleFileName());
        clmDuplicateCount.setCellValueFactory(cellData -> cellData.getValue().getCountRelatedImages());
    }

    @FXML
    protected void doBrowseButton(ActionEvent event) {

        log.info(lblDirectory.getText());

        DirectoryChooser dc = new DirectoryChooser();
        File dir = new File(lblDirectory.getText());

        dc.setInitialDirectory((dir.isDirectory()) ? dir : new File(System.getProperty("user.home")));
        dc.setTitle("Picture Directory");

        log.info(dc.getInitialDirectory().getPath());
        File selectedDirectory = dc.showDialog(((Node) event.getTarget()).getScene().getWindow());

        lblDirectory.setText(selectedDirectory.getPath());
    }

    @FXML
    protected void doSearchButton(ActionEvent event) {

        btnBrowse.setDisable(true);
        btnSearch.setDisable(true);
        slidThreshold.setDisable(true);
        tblResults.setDisable(true);

        Task searchWorker = searchWorker();
        progressBar.progressProperty().unbind();
        progressBar.progressProperty().bind(searchWorker.progressProperty());
        lblProgress.textProperty().unbind();
        lblProgress.textProperty().bind(searchWorker.messageProperty());
        tblResults.setItems(imageSearchResults);

        searchWorker.setOnSucceeded(event1 -> {
            btnBrowse.setDisable(false);
            btnSearch.setDisable(false);
            slidThreshold.setDisable(false);
            tblResults.setDisable(false);
        });

        new Thread(searchWorker).start();

        event.consume();
    }

    private Task searchWorker() {
        return new Task() {
            @Override
            public Void call() {
                updateMessage("Finding Files");
                foundImages.clear();

                ForkJoinPool pool = new ForkJoinPool();
                pool.invoke(new GetFiles(new File(lblDirectory.getText()), cbxSubFolders.isSelected()));

                progressBarCurrent = 0;
                progressBarMax = foundImages.size();

                updateMessage("Creating Color Signature");
                imageSearchResults.clear();

                foundImages.parallelStream().forEach(file -> {
                    progressBarCurrent++;
                    updateMessage("Creating Color Signature: " + progressBarCurrent + "/" + progressBarMax);
                    updateProgress(progressBarCurrent, progressBarMax);
                    buildImageEntry(file);
                });
                /*for (File file : foundImages) {
                    progress++;
                    buildImageEntry(file);
                    updateProgress(progress, progressBarMax);
                }*/

                foundImages.clear();
                duplicateImages.clear();

                updateMessage("Comparing Images");
                progressBarCurrent = 0;
                progressBarMax = imageSearchResults.size();

                for (ImageEntry imageEntry : imageSearchResults) {
                    progressBarCurrent++;
                    if (duplicateImages.contains(imageEntry)) {
                        updateProgress(progressBarCurrent, progressBarMax);
                        continue;
                    }
                    List<ImageEntry> subList = imageSearchResults.subList(progressBarCurrent, imageSearchResults.size());
                    findDuplicates(imageEntry, subList);
                    updateProgress(progressBarCurrent, progressBarMax);
                }
                updateMessage("Building Results");
                imageSearchResults.removeAll(duplicateImages);

                updateMessage("Completed");
                updateProgress(progressBarMax, progressBarMax);
                return null;
            }
        };
    }

/*    static class CalculateSignature extends RecursiveAction {
        private final Logger log = Logger.getLogger( this.getClass().getName());
        private final List<File> files;
        CalculateSignature(List<File> files) {
            this.files = files;
        }

        private final static int THRESHOLD = 20;
        @Override
        protected void compute() {
            if (files.size() < THRESHOLD) {
                files.parallelStream().forEach(file -> buildImageEntry(file));
                //for (File file : files) {
                //    buildImageEntry(file);
                //}
            } else {
                invokeAll(new CalculateSignature(files.subList(0, THRESHOLD)),
                          new CalculateSignature(files.subList(THRESHOLD + 1, files.size())));
            }
        }

        private void buildImageEntry(File file) {
            try {
                BufferedImage bi = ImageIO.read(file);
                ImageEntry ie = new ImageEntry(file, CompareImages.calcSignature(bi));
                imageSearchResults.add(ie);
            } catch (IOException io) {
                log.info("buildImageEntry [File Not Found] " + io.getMessage());
                io.printStackTrace();
            } catch (Exception e) {
                log.info("buildImageEntry [Exception] " + file.getAbsolutePath() + " " + e.getMessage());
                e.printStackTrace();
            }
        }
    }*/

    private void buildImageEntry(File file) {
        try {
            BufferedImage bi = ImageIO.read(file);
            ImageEntry ie = new ImageEntry(file, CompareImages.calcSignature(bi));
            imageSearchResults.add(ie);
        } catch (IOException io) {
            log.info("buildImageEntry [File Not Found] " + io.getMessage());
            io.printStackTrace();
        } catch (Exception e) {
            log.info("buildImageEntry [Exception] " + file.getAbsolutePath() + " " + e.getMessage());
            e.printStackTrace();
        }
    }

    private void findDuplicates(ImageEntry imageEntry, List<ImageEntry> subList) {

        int index = imageSearchResults.indexOf(imageEntry);
        for (ImageEntry imageEntry2 : subList) {
            double difference = CompareImages.calcDifference(imageEntry.getSignature(), imageEntry2.getSignature());
            if (difference < slidThreshold.getValue()) {
                imageEntry.addRelatedImage(imageEntry2, Double.toString(difference));
                //imageSearchResults.get(index).addRelatedImage(imageEntry2, Double.toString(difference));
                duplicateImages.add(imageEntry2);
            }
        }

        if (imageEntry.getRelatedImages().size() == 0) {
            duplicateImages.add(imageEntry);
        } else {
            imageSearchResults.set(index, imageEntry);
        }
    }

    @FXML
    protected void doShowResult(MouseEvent event) {
        if (tblResults.getSelectionModel().getSelectedItem() == null) {
            return;
        }
        ImageEntry selectedItem = tblResults.getSelectionModel().getSelectedItem();
        try {
            BufferedImage bi = ImageIO.read(selectedItem.getFile());
            int imgWidth = bi.getWidth();
            int imgHeight = bi.getHeight();

            bi = CompareImages.createThumbnail(bi,
                    CompareImages.getScaledDimension(new Dimension(imgWidth, imgHeight),
                            new Dimension((int) Math.round(imgResult.getFitWidth()), (int) Math.round(imgResult.getFitHeight()))));

            imgResult.setImage(SwingFXUtils.toFXImage(bi, null));

            HBox hBox = new HBox();

            addImageToResultPane(hBox, selectedItem, "Original");

            for (Map.Entry<ImageEntry, String> duplicateImage : selectedItem.getRelatedImages().entrySet()) {
                addImageToResultPane(hBox, duplicateImage.getKey(), duplicateImage.getValue());
            }

            sPaneResults.setContent(hBox);

        } catch (IOException ie) {
            log.info("File no longer exists: " + ie);
            imageSearchResults.remove(selectedItem);
        } catch (Exception e) {
            log.info("showResult - Exception: " + e);
            e.printStackTrace();
        }

        event.consume();
    }

    private void addImageToResultPane(HBox hBox, ImageEntry imageEntry, String difference) {
        try {

            BufferedImage bi = ImageIO.read(imageEntry.getFile());
            int imgWidth = bi.getWidth();
            int imgHeight = bi.getHeight();

            bi = CompareImages.createThumbnail(bi,
                    CompareImages.getScaledDimension(new Dimension(imgWidth, imgHeight),
                            new Dimension((int) Math.round(imgResult.getFitWidth()), (int) Math.round(imgResult.getFitHeight()))));

            ImageView imageView = new ImageView(SwingFXUtils.toFXImage(bi, null));
            Tooltip.install(imageView, new Tooltip("File: " + imageEntry.getSimpleFileName().get() +
                    "\n" + "Difference: " + difference +
                    "\n" + "Path: " + imageEntry.getFile().getAbsolutePath() +
                    "\n" + "Dimensions: " + imgWidth + "x" + imgHeight));
            imageView.setOnMouseClicked(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    if (mouseEvent.getButton().equals(MouseButton.PRIMARY)) {
                        if (mouseEvent.getClickCount() == 2) {
                            try {
                                Desktop.getDesktop().open(imageEntry.getFile());
                            } catch (IOException io) {
                                log.info("Error opening image file: " + io);
                            }
                        }
                    }
                    if (mouseEvent.getButton().equals(MouseButton.SECONDARY)) {
                        ContextMenu contextMenu = new ContextMenu();
                        MenuItem menuItem = new MenuItem("Delete Image");

                        menuItem.setOnAction(e -> {
                            if (difference.equals("Original")) {
                                imageSearchResults.remove(imageEntry);
                                tblResults.getSelectionModel().clearSelection();
                                imgResult.setImage(null);
                                sPaneResults.setContent(null);
                            }
                            hBox.getChildren().remove(imageView);
                            imageEntry.getFile().delete();
                        });

                        contextMenu.getItems().add(menuItem);
                        contextMenu.show(imageView, mouseEvent.getScreenX(), mouseEvent.getScreenY());
                    }
                }
            });

            hBox.getChildren().add(imageView);
            hBox.getChildren().add(new Box(10, 0, 0));

        } catch (IOException ie) {
            log.info("File no longer exists: " + ie);
            imageSearchResults.remove(imageEntry);
        } catch (Exception e) {
            log.info("showResult - Exception: " + e);
            e.printStackTrace();
        }
    }

    static class GetFiles extends RecursiveAction {
        private final Logger log = Logger.getLogger(this.getClass().getName());
        private final File getFilesDirectory;
        private final Boolean checkSubFolders;

        GetFiles(File directory, Boolean checkSubFolders) {
            this.getFilesDirectory = directory;
            this.checkSubFolders = checkSubFolders;
        }

        @Override
        protected void compute() {
            try {
                if (getFilesDirectory.isDirectory()) {
                    Pattern pattern = Pattern.compile(IMAGE_PATTERN);
                    if (getFilesDirectory.list().length > 0) {
                        foundImages.addAll(Arrays.asList(getFilesDirectory.listFiles((File file) -> {
                            Matcher matcher = pattern.matcher(file.getName());
                            return matcher.matches();
                        })));

                        if (checkSubFolders) {
                            for (File file2 : getFilesDirectory.listFiles()) {
                                if (file2.isDirectory()) {
                                    invokeAll(new GetFiles(file2, true));
                                }
                            }
                        }
                    }
                }
            } catch (NullPointerException npe) {
                log.info("Unable to access directory: " + getFilesDirectory.getName());
            }
        }
    }

}
